package IC.SemanticChecks;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import IC.BinaryOps;
import IC.SemanticError;
import IC.UnaryOps;
import IC.AST.ArrayLocation;
import IC.AST.Assignment;
import IC.AST.Break;
import IC.AST.CallStatement;
import IC.AST.Continue;
import IC.AST.Expression;
import IC.AST.ExpressionBlock;
import IC.AST.Field;
import IC.AST.Formal;
import IC.AST.ICClass;
import IC.AST.If;
import IC.AST.Length;
import IC.AST.LibraryMethod;
import IC.AST.Literal;
import IC.AST.LocalVariable;
import IC.AST.LogicalBinaryOp;
import IC.AST.LogicalUnaryOp;
import IC.AST.MathBinaryOp;
import IC.AST.MathUnaryOp;
import IC.AST.Method;
import IC.AST.NewArray;
import IC.AST.NewClass;
import IC.AST.PrimitiveType;
import IC.AST.Program;
import IC.AST.Return;
import IC.AST.Statement;
import IC.AST.StatementsBlock;
import IC.AST.StaticCall;
import IC.AST.StaticMethod;
import IC.AST.This;
import IC.AST.UnaryOp;
import IC.AST.UserType;
import IC.AST.VariableLocation;
import IC.AST.VirtualCall;
import IC.AST.VirtualMethod;
import IC.AST.Visitor;
import IC.AST.While;
import IC.Symbols.ClassSymbolTable;
import IC.Symbols.GlobalSymbolTable;
import IC.Symbols.Symbol;
import IC.Symbols.SymbolTable;
import IC.Types.ArrayType;
import IC.Types.ClassType;
import IC.Types.MethodType;
import IC.Types.Type;
import IC.Types.TypeTable;
public class TypeCheckingVisitor implements Visitor {

	private GlobalSymbolTable globalSymbolTable;

	@Override
	public Object visit(Program program) {
		globalSymbolTable = (GlobalSymbolTable) program.getEnclosingScope();
		List<ICClass> listOfClasses = program.getClasses();
		Iterator<ICClass> iter = listOfClasses.iterator();
		while (iter.hasNext()){
			if (iter.next().accept(this) == null){
				return null;
			}
		}
		return true;
	}

	@Override
	public Object visit(ICClass icClass) {
		Type type = TypeTable.classType(icClass.getName());
		icClass.setNodeType(type);
		List<Method> listOfMethods = icClass.getMethods();
		List<Field> listOfFields = icClass.getFields();

		Iterator<Method> methodIter =listOfMethods.iterator();
		Iterator<Field> fieldIter =listOfFields.iterator();

		while (methodIter.hasNext()){
			if (methodIter.next().accept(this) == null){
				return null;
			}
		}
		while (fieldIter.hasNext()){
			if (fieldIter.next().accept(this) == null){
				return null;
			}
		}

		return true;
	}

	@Override
	public Object visit(Field field) {
		Type resultType = field.getEnclosingScope().lookup(field.getName()).getType();
		if (resultType == null){
			System.err.println(new SemanticError(field.getLine(), "Error in field: field type could not be resolved"));
			return null;
		}
		field.setNodeType(resultType);
		return resultType;
	}

	@Override
	public Object visit(VirtualMethod method) {
		return visitMethods(method);
	}

	@Override
	public Object visit(StaticMethod method) {
		return visitMethods(method);
	}

	@Override
	public Object visit(LibraryMethod method) {
		return visitMethods(method);
	}

	@Override
	public Object visit(Formal formal) {
		Type resultType = (Type) formal.getType().accept(this);
		formal.setNodeType(resultType);
		return resultType;
	}

	@Override
	public Object visit(PrimitiveType type) {
		Type resultType = null;
		try {  //no exception should occur - its primitive.
			resultType = TypeTable.getType(type);
			type.setNodeType(resultType);
		} catch (SemanticError e) {
			return null;
		}
		return resultType;
	}

	@Override
	public Object visit(UserType type) {
		Type result = null;
		try {
			result =  TypeTable.getType(type);
			type.setNodeType(result);
		} catch (SemanticError e) {
			return null;
		}
		return result;

	}

	@Override
	public Object visit(Assignment assignment) {
		Type variable = (Type) assignment.getVariable().accept(this);
		Type assign = (Type) assignment.getAssignment().accept(this);

		if (variable == null || assign == null){
			return null;
		}
		

		if (!assign.subTypeOf(variable)){
			System.err.println(new SemanticError(assignment.getLine(), "Error in assignment: Assignment has to be a sub type of the variable"));
			return null;
		}

		return true;
	}

	@Override
	public Object visit(CallStatement callStatement) {

		if (callStatement.getCall() instanceof VirtualCall){
			return ((VirtualCall) callStatement.getCall()).accept(this);
		}
		else if (callStatement.getCall() instanceof StaticCall){
			return ((StaticCall) callStatement.getCall()).accept(this);			
		}
		else {
			return null;
		}
	}

	@Override
	public Object visit(Return returnStatement) {
		Type retType = null;
		Type retTypeOrigin =  returnStatement.getEnclosingScope().lookup("ret").getType();

		if (returnStatement.hasValue()){
			retType = (Type) returnStatement.getValue().accept(this);
		}

		if (retTypeOrigin == TypeTable.getVoidType() && retType != null && retType !=  TypeTable.getVoidType()){
			System.err.println(new SemanticError(returnStatement.getLine(), "Error in returnStatement: return type isn't void as expected"));
			return null;
		}
		
		else if (retTypeOrigin != TypeTable.getVoidType() && !returnStatement.hasValue()){
			System.err.println(new SemanticError(returnStatement.getLine(), "Error in returnStatement: return type isn't as expected"));
			return null;
		}

		else if (retTypeOrigin != TypeTable.getVoidType() && retType != null && !retType.subTypeOf(retTypeOrigin)){
			System.err.println(new SemanticError(returnStatement.getLine(), "Error in returnStatement: return value isn't from type expected"));
			return null;
		}
		
		else if (retType == null && retTypeOrigin!= TypeTable.getVoidType()){
			return null;
		}

		return true;
	}

	@Override
	public Object visit(If ifStatement) {
		Type cond = (Type) ifStatement.getCondition().accept(this);

		if (cond == null){
			return null;
		}

		if (cond != TypeTable.getBoolType()){
			System.err.println(new SemanticError(ifStatement.getLine(), "Error in ifStatement: Condition's type has to be boolean"));
			return null;
		}

		if (ifStatement.getOperation().accept(this) == null){
			return null;
		}

		if (ifStatement.hasElse() && ifStatement.getElseOperation().accept(this) == null){
			return null;
		}

		return true;
	}

	@Override
	public Object visit(While whileStatement) {
		Type cond= (Type) whileStatement.getCondition().accept(this);

		if (cond == null){
			return null;
		}

		if (cond != TypeTable.getBoolType()){
			System.err.println(new SemanticError(whileStatement.getLine(), "Error in while: Condition's type has to be boolean"));
			return null;
		}

		if (whileStatement.getOperation().accept(this) == null){
			return null;
		}

		return true;
	}

	@Override
	public Object visit(Break breakStatement) {
		return true;
	}

	@Override
	public Object visit(Continue continueStatement) {
		return true;
	}

	@Override
	public Object visit(StatementsBlock statementsBlock) {

		List<Statement> listOfStmt = statementsBlock.getStatements();
		Iterator<Statement> iter = listOfStmt.iterator();
		while (iter.hasNext()){
			if (iter.next().accept(this) == null){
				return null;
			}
		}

		return true;
	}

	@Override
	public Object visit(LocalVariable localVariable) {
		Type init = null;
		Type type = null;
		if (localVariable.hasInitValue() && (init = (Type) localVariable.getInitValue().accept(this)) == null){
			return null;
		}

		if ((type = (Type) localVariable.getType().accept(this)) == null){
			return null;
		}


		if (init != null && !init.subTypeOf(type)){ 
			System.err.println(new SemanticError(localVariable.getLine(), "Error in LocalVariable: init value isn't sub type of variable"));
			return null;
		}

		localVariable.setNodeType(type);
		return true;
	}

	@Override
	public Object visit(VariableLocation location) {
		Type type = null;

		if (location.isExternal()){
			if ((type = (Type) location.getLocation().accept(this)) == null){
				return null; /* no class type was found */
			}

			if (!(type instanceof ClassType)){
				System.err.println(new SemanticError(location.getLine(), "Error in VariableLocation: location's type isn't class type"));
				return null;
			}
			
			Symbol returnedSymbol = getClassSymbolTable((ClassType) type).lookup(location.getName(), location.getOrder()); 
			if (returnedSymbol == null){
				return null;
			}
			
			Type resultType = returnedSymbol.getType();
			location.setNodeType(resultType);
			return resultType;
		}

		else {
			if (location.getEnclosingScope() != null && (location.getEnclosingScope().lookup(location.getName(), location.getOrder())) != null){
				Type resultType = location.getEnclosingScope().lookup(location.getName(), location.getOrder()).getType();
				location.setNodeType(resultType);
				return resultType;
			}
			else{
				try {
					type =  TypeTable.getType(location);
				} catch (SemanticError e) {
					return null;
				}
				location.setNodeType(type);
				return type;
			}
		}
		
	}

	@Override
	public Object visit(ArrayLocation location) {

		Type array = (Type) location.getArray().accept(this);
		Type index = (Type) location.getIndex().accept(this);

		if (index == null || array == null){
			return null;
		}

		else if (!(array instanceof ArrayType)){
			System.err.println(new SemanticError(location.getLine(), "Error in Arraylocation: location isn't from type array"));						
		}
		else if (index != TypeTable.getIntType()){
			System.err.println(new SemanticError(location.getLine(), "Error in Arraylocation: Index isn't from type int"));
		}
		
		else {
			Type type = ((ArrayType)array).getElementType();
			location.setNodeType(type);
			return type;
		}

		return null;
	}

	@Override
	public Object visit(StaticCall call) {
		MethodType methodType = null;

		ClassType classType = (ClassType) globalSymbolTable.lookup(call.getClassName()).getType();
		methodType = (MethodType) getClassSymbolTable(classType).lookup(call.getName()).getType();

		if (methodType == null || classType == null){
			return null;
		}


		if (call.getArguments().size() != methodType.getParamTypes().length){
			System.err.println(new SemanticError(call.getLine(), "Error in StaticCall: incompatible number of arguments"));;
			return null;
		}

		List<Expression> listOfArguments = call.getArguments();
		Iterator<Expression> iter = listOfArguments.iterator();

		int index=0;
		while(iter.hasNext()){
			Type typeInMethod = methodType.getParamTypes()[index];
			Type typeInCall;
			if ((typeInCall = (Type) iter.next().accept(this)) == null){
				return null;
			}
			if (!typeInCall.subTypeOf(typeInMethod)){
				System.err.println(new SemanticError(call.getLine(), "Error in VirtualCall: invalid type of paramater"));;
				return null;
			}
			index++;
		}

		Type type = methodType.getReturnType();
		call.setNodeType(type);
		return type;
	}

	@Override
	public Object visit(VirtualCall call) {

		MethodType methodType = null;

		if (call.isExternal()){
			ClassType classType = (ClassType) call.getLocation().accept(this);
			if (classType == null){
				return null;
			}
			if ((methodType = (MethodType) getClassSymbolTable(classType).lookup(call.getName(), call.getOrder()).getType()) == null){
				return null;
			}
		}

		else {
			methodType = (MethodType) call.getEnclosingScope().lookupMethod(call.getName(), call.getOrder()).getType();
		}

		if (call.getArguments().size() != methodType.getParamTypes().length){
			System.err.println(new SemanticError(call.getLine(), "Error in VirtualCall: incompatible number of arguments"));;
			return null;
		}

		List<Expression> listOfArguments = call.getArguments();
		Iterator<Expression> iter = listOfArguments.iterator();

		int index=0;
		while(iter.hasNext()){
			Type typeInMethod = methodType.getParamTypes()[index];
			Type typeInCall;
			if ((typeInCall = (Type) iter.next().accept(this)) == null){
				return null;
			}
			if (!typeInCall.subTypeOf(typeInMethod)){
				System.err.println(new SemanticError(call.getLine(), "Error in VirtualCall: invalid type of paramater"));;
				return null;
			}
			index++;
		}

		Type type = methodType.getReturnType();
		call.setNodeType(type);
		return type;
	}

	@Override
	public Object visit(This thisExpression) {
		SymbolTable currentSymbolTable = thisExpression.getEnclosingScope();
		while (!(currentSymbolTable instanceof ClassSymbolTable)){
			currentSymbolTable = currentSymbolTable.getParent();
		}
		Type type = globalSymbolTable.lookup(currentSymbolTable.getId()).getType();
		thisExpression.setNodeType(type);
		return type;
	}

	@Override
	public Object visit(NewClass newClass) {
		if (globalSymbolTable.lookup(newClass.getName()) == null){
			System.err.println(new SemanticError(newClass.getLine(), "Error in NewClass: Class type couldn't be found"));			
			return null;
		}

		else {
			Type type = globalSymbolTable.lookup(newClass.getName()).getType();
			newClass.setNodeType(type);
			return type;
		}
	}

	@Override
	public Object visit(NewArray newArray) {

		Type newArr = null;
		Type index = null;
		try {
			newArr = TypeTable.getType(newArray);
		} catch (SemanticError e) {
			return null;
		}

		if ((index = (Type) newArray.getSize().accept(this)) == null){
			return null;
		}

		if ((index = (Type) newArray.getSize().accept(this)) == TypeTable.getIntType()){
			newArray.setNodeType(newArr);
			return newArr;
		}
		else {
			System.err.println(new SemanticError(newArray.getLine(), "Error in NewArray: Cannot create an array with size not int"));
		}
		return null;
	}

	@Override
	public Object visit(Length length) {
		
		Type array = (Type) length.getArray().accept(this);
			
		if (array != null && array instanceof ArrayType){
			return TypeTable.getIntType(); 
		}

		System.err.println(new SemanticError(length.getLine(), "Error in Length: Cannot return length on type"));		
		return null;
	}

	@Override
	public Object visit(MathBinaryOp binaryOp) {
		Type firstOperand = (Type) binaryOp.getFirstOperand().accept(this);
		Type secondOperand = (Type) binaryOp.getSecondOperand().accept(this);
		BinaryOps opeartion = binaryOp.getOperator();


		if (firstOperand == null || secondOperand == null){
			return null;
		}

		switch(opeartion){
		case PLUS:
			if (firstOperand == TypeTable.getIntType() && secondOperand == TypeTable.getIntType()){
				Type type = TypeTable.getIntType();
				binaryOp.setNodeType(type);
				return type;
			}
			else if (firstOperand == TypeTable.getStringType() && secondOperand == TypeTable.getStringType()){
				Type type = TypeTable.getStringType();
				binaryOp.setNodeType(type);
				return type;
			}

			//			else if (firstOperand == TypeTable.getStringType() && secondOperand == TypeTable.getIntType() ||
			//					firstOperand == TypeTable.getIntType() && secondOperand == TypeTable.getStringType()){
			//				return TypeTable.getStringType();
			//			}
			else {
				System.err.println(new SemanticError(binaryOp.getLine(), "Error in MathBinaryOp: Cannot perform PLUS opearion on types not both int/string"));
			}
			break;
		case MINUS:
		case DIVIDE:
		case MULTIPLY:
		case MOD:
			if (firstOperand == TypeTable.getIntType() && secondOperand == TypeTable.getIntType()){
				Type type = TypeTable.getIntType();
				binaryOp.setNodeType(type);
				return type;
			}
			else{
				System.err.println(new SemanticError(binaryOp.getLine(), "Error in MathBinaryOp: Cannot perform " + opeartion.toString() + " opearion on types not int"));
			}
			break;
		case EQUAL:
		case GT:
		case GTE:
		case LAND:
		case LOR:
		case LT:
		case LTE:
		case NEQUAL:
		}

		return null;
	}


	@Override
	public Object visit(LogicalBinaryOp binaryOp) {
		Type firstOperand = (Type) binaryOp.getFirstOperand().accept(this);
		Type secondOperand = (Type) binaryOp.getSecondOperand().accept(this);
		BinaryOps operation = binaryOp.getOperator();

		if (firstOperand == null || secondOperand == null){
			return null;
		}

		switch(operation){
		case LTE:
		case LT:
		case GTE:
		case GT:

			if (firstOperand == TypeTable.getIntType() && secondOperand == TypeTable.getIntType()){
				Type type = TypeTable.getBoolType();
				binaryOp.setNodeType(type);
				return type;
			}
			else {
				System.err.println(new SemanticError(binaryOp.getLine(), "Error in LogicalBinaryOp: Cannot perform " + operation.toString() + " on types not int"));
			}
			break;
		case LAND:
		case LOR:
			if (firstOperand == TypeTable.getBoolType() && secondOperand == TypeTable.getBoolType()){
				Type type = TypeTable.getBoolType();
				binaryOp.setNodeType(type);
				return type;
			}
			else {
				System.err.println(new SemanticError(binaryOp.getLine(), "Error in LogicalBinaryOp: Cannot perform " + operation.toString() + " on types not boolean"));				
			}
			break;
		case EQUAL:
		case NEQUAL:
			if (firstOperand == TypeTable.getNullType() && secondOperand instanceof ClassType ||
					firstOperand instanceof ClassType && secondOperand == TypeTable.getNullType()){
				return TypeTable.getBoolType();
			}	

			else if (firstOperand.subTypeOf(secondOperand) || secondOperand.subTypeOf(firstOperand)){
				return TypeTable.getBoolType();
			}

			else {
				System.err.println(new SemanticError(binaryOp.getLine(), "Error in LogicalBinaryOp: Cannot perform " + operation.toString() + ", no class/type is a subtype of the other"));				
			}
			break;
		case DIVIDE:
		case MINUS:
		case MOD:
		case MULTIPLY:
		case PLUS:
			System.err.println(new SemanticError(binaryOp.getLine(), "Opeartion " + operation.toString() + " isn't a logical operation"));
		}
		return null;
	}

	@Override
	public Object visit(MathUnaryOp unaryOp) {
		return visitUnaryOperation(unaryOp);
	}


	@Override
	public Object visit(LogicalUnaryOp unaryOp) {
		return visitUnaryOperation(unaryOp);
	}

	@Override
	public Object visit(Literal literal) {
		Type resultType = null;
		try {  //no exception should occur - its primitive.
			resultType = TypeTable.getType(literal);
			literal.setNodeType(resultType);
		} catch (SemanticError e) {
			System.err.println(e);
			return null;
		}
		return resultType;
	}

	@Override
	public Object visit(ExpressionBlock expressionBlock) {
		Object res = expressionBlock.getExpression().accept(this);
		if (res instanceof Type){
			expressionBlock.setNodeType((Type) res);
		}
		return res;
		
	}


	private Object visitUnaryOperation(UnaryOp unaryOp){

		Type operand = (Type) unaryOp.getOperand().accept(this);
		UnaryOps operator = unaryOp.getOperator();

		if (operand == null){
			return null;
		}

		switch(operator){
		case UMINUS:
			if (operand == TypeTable.getIntType()){
				Type type = TypeTable.getIntType();;
				unaryOp.setNodeType(type);
				return type;
			} 
			else{
				System.err.println(new SemanticError(unaryOp.getLine(), "Error in UnaryOp: Cannot perform UMINUS opearion on type " + operand.toString()));
			}
			break;
		case LNEG:
			if (operand == TypeTable.getBoolType()){
				Type type = TypeTable.getBoolType();;
				unaryOp.setNodeType(type);
				return type;
			}
			else {
				System.err.println(new SemanticError(unaryOp.getLine(), "Error in UnaryOp: Cannot perform LNEG opearion on type " + operand.toString()));
			}
			break;
		}

		return null;
	}

	private Object visitMethods(Method method){
		
		try {
			Type methodType = TypeTable.getType(method);
			method.setNodeType(methodType);
		} catch (SemanticError e) {
			System.err.println(e);
			return null;
		}
		List<Formal> listOfFormals = method.getFormals();
		Iterator<Formal> formalIter = listOfFormals.iterator();


		while (formalIter.hasNext()){
			if (formalIter.next().accept(this) == null){
				return null;
			}

		}

		if (!(method instanceof LibraryMethod)){
			List <Statement> listOfStatements = method.getStatements();
			Iterator<Statement> stmtIter = listOfStatements.iterator();
			while (stmtIter.hasNext()){
				if (stmtIter.next().accept(this) == null){
					return null;
				}
			}
		}

		return true;
	}

	private SymbolTable getClassSymbolTable(ClassType classType){
		
		List<String> classHierarchy = new ArrayList<String>();
		
		ClassType currentType = classType;
		classHierarchy.add(currentType.getName());
		String superClassName = currentType.getSuperClassName();

		while (superClassName != null){
			classHierarchy.add(superClassName);
			currentType = currentType.getSuperClass();
			superClassName = currentType.getSuperClassName();
		}
		
		SymbolTable currentSymbolTable = globalSymbolTable;
		for (int i = classHierarchy.size() - 1; i >= 0; i--){
			currentSymbolTable = currentSymbolTable.getChild(classHierarchy.get(i));
		}
		return currentSymbolTable;
	}

}
